那今天我們就來使用bloc
及flutter_bloc
這兩個來實作範例,基本上我們在實作BLoC pattern時我們都會切分成三層分別是:資料層、BLoC層、UI層。
那這次我們直接來看官方提供其中一個範例:無限滾動列表
這次會用到的套件
dependencies:
freezed_annotation: ^0.14.3
dio: ^4.0.0
bloc: ^7.2.1
flutter_bloc: ^7.3.0
equatable: ^2.0.3
bloc_concurrency: ^0.1.0
stream_transform: ^2.0.0
dev_dependencies:
build_runner: ^2.1.4
freezed: ^0.14.5
json_serializable: ^5.0.2
我們一樣使用 jsonPlaceholder 加上quicktype 的資料產出這個 model
// To parse this JSON data, do
//
// final post = postFromJson(jsonString);
import 'package:equatable/equatable.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:convert';
part 'post.freezed.dart';
part 'post.g.dart';
List<Post> postFromJson(String str) =>
List<Post>.from(json.decode(str).map((x) => Post.fromJson(x)));
String postToJson(List<Post> data) =>
json.encode(List<dynamic>.from(data.map((x) => x.toJson())));
@freezed
abstract class Post with _$Post {
const factory Post({
int? userId,
int? id,
String? title,
String? body,
}) = _Post;
factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);
}
首先我們需要新增三個檔案分別代表「事件」、「狀態」、「BLoC」
// post_event.dart
part of 'post_bloc.dart';
abstract class PostEvent extends Equatable {
@override
List<Object> get props => [];
}
class PostFetched extends PostEvent {}
// post_state.dart
part of 'post_bloc.dart';
enum PostStatus { initial, success, failure }
class PostState extends Equatable {
const PostState({
this.status = PostStatus.initial,
this.posts = const <Post>[],
this.hasReachedMax = false,
});
final PostStatus status;
final List<Post> posts;
final bool hasReachedMax;
PostState copyWith({
PostStatus? status,
List<Post>? posts,
bool? hasReachedMax,
}) {
return PostState(
status: status ?? this.status,
posts: posts ?? this.posts,
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
);
}
@override
String toString() {
return '''PostState { status: $status, hasReachedMax: $hasReachedMax, posts: ${posts.length} }''';
}
@override
List<Object> get props => [status, posts, hasReachedMax];
}
首先來看「事件」及「狀態」
PostEvent
就是我們這個BLoC會接收到的所以事件的父類,而這裡繼承了 Equatable
是為了能讓我們的在比較兩個 instance時可以正確的比對,因為就算我們傳入一模一樣的值進入同一個constructor 還是會產生兩個不一樣的實例,Equatable
override了 ==
及 hashcode
讓我們可以能夠變成「值一樣就代表時同一個instance」。
那我們這裡就只要有一個事件: PostFetched
接下來看到狀態,我們一樣繼承了 Equatable
,然後我們的狀態有三個值: status
、 posts
、 hasReachedMax
來表示fetch的狀態、存放Post的值以及是否讀取到最後了。
這邊最主要是實作了 copyWith
這個方法,因為每次我們要從BLoC的送出資料時都是送出「完整一份狀態」,也就是利用immutable的概念。
所以為了減少麻煩如果我只要更改其中一種field我只要 copyWith
後然後傳入我們要變更的field及數值就好。
最後就來看看我們的BLoC
import 'dart:convert';
import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_rest_api_playground/model/post/post.dart';
import 'package:flutter_rest_api_playground/service/http.dart';
import 'package:stream_transform/stream_transform.dart';
part 'post_event.dart';
part 'post_state.dart';
const _postLimit = 20;
const throttleDuration = Duration(milliseconds: 100);
EventTransformer<E> throttleDroppable<E>(Duration duration) {
return (events, mapper) {
return droppable<E>().call(events.throttle(duration), mapper);
};
}
class PostBloc extends Bloc<PostEvent, PostState> {
PostBloc() : super(const PostState()) {
on<PostFetched>(
_onPostFetched,
transformer: throttleDroppable(throttleDuration),
);
}
final HttpService _httpService = HttpService();
Future<void> _onPostFetched(
PostFetched event, Emitter<PostState> emit) async {
if (state.hasReachedMax) return;
try {
if (state.status == PostStatus.initial) {
final posts = await _fetchPosts();
return emit(state.copyWith(
status: PostStatus.success,
posts: posts,
hasReachedMax: false,
));
}
final posts = await _fetchPosts(state.posts.length);
emit(posts.isEmpty
? state.copyWith(hasReachedMax: true)
: state.copyWith(
status: PostStatus.success,
posts: List.of(state.posts)..addAll(posts),
hasReachedMax: false,
));
} catch (_) {
emit(state.copyWith(status: PostStatus.failure));
}
}
Future<List<Post>> _fetchPosts([int startIndex = 0]) async {
final response = await _httpService.get(
'/posts',
queryParameters: {'_start': '$startIndex', '_limit': '$_postLimit'},
);
final jsonStr = json.encode(response.data);
final result = postFromJson(jsonStr);
return result;
}
}
首先我們先實例化一個這個BLoC私有的 _httpService
做為我們call api 的 client。
首先先來實作 _fetchPosts
這個call api 的method , 主要就是封裝了資料轉換及傳入 queryParameters
。
然後就是要來實作event handler: _onPostFetched
首先會看到如果我們讀到最後了就會直接return 不 emit
也就代表 UI層那邊不會收到這件事情,接下來就是做初次的fetch,這裡會看到我們用 emit
包裹我們要送出的狀態,這裡就用 copyWith
來讓我們創造一份新的狀態。
接下來就是實作接下來正常的每次fetch,其實也只是繼續用emit
將狀態送出。這裡會用到 ..
casecade 運算子,因為有些method不會回傳值就只是單純的mutate操作,但使用..
就能直接回傳那個instance。
今天的程式碼:
https://github.com/zxc469469/flutter_rest_api_playground/tree/Day28
今天就是直接從官方範例來做這個Demo,看看做完後還能不能再額外加什麼功能之類的。
明天我們就繼續來說明 UI層的實作
參考資料:
https://bloclibrary.dev/#/flutterinfinitelisttutorial